#include "Common.h"
#include "Mesh.h"
#include "MeshManager.h"
#include "Utils/Tokenizer.h"
#include "Utils/Util.h"
#include "Utils/Logger/Logger.h"

using namespace RayFrame;
using namespace glm;

IMPL_SINGLETON(MeshManager)

MeshManager::MeshManager()
{

}

MeshManager::~MeshManager()
{
}

float readFloat(Tokenizer &tok){
  char *str = tok.next();
  if (str[0] == '-'){
    str = tok.next();
    return -(float) atof(str);
  } else {
    if (str[0] == '+'){
      str = tok.next();
    }
    return (float) atof(str);
  }
}

unsigned *rearrange(RefArray <ObjMaterial> &materials, const unsigned *srcIndices, const unsigned nIndices){
  unsigned *indices = new unsigned[nIndices];

  unsigned currIndex = 0;
  for (unsigned i = 0; i < materials.GetCount(); i++){
    RefArray <unsigned> *mInds = materials[i].indices;
    for (unsigned j = 0; j < mInds->GetCount(); j += 2){
      memcpy(indices + currIndex, srcIndices + (*mInds)[j], (*mInds)[j + 1] * sizeof(unsigned));
      currIndex += (*mInds)[j + 1];
    }
  }

  assert(currIndex == nIndices);

  return indices;
}

unsigned getObjMaterial(RefArray <ObjMaterial> &materials, const char *str){
  for (unsigned i = 0; i < materials.GetCount(); i++){
    if (strcmp(str, materials[i].name) == 0){
      return i;
    }
  }
  ObjMaterial mat;
  mat.name = new char[strlen(str) + 1];
  mat.indices = new RefArray <unsigned>();
  strcpy(mat.name, str);
  return materials.Add(mat);
}

Mesh* MeshManager::LoadObj(STRING modelname, STRING filename)
{
  Mesh* model = new Mesh;
  InsertData(modelname, model);
  doLoadObj(modelname, filename, *model);
  return model;
}

void MeshManager::LoadObj(STRING modelname, STRING filename, Mesh& model)
{
  doLoadObj(modelname, filename, model);
}

void MeshManager::doLoadObj(STRING modelname, STRING filename, Mesh& model)
{
  if(!SearchResource(filename, "Meshes"))
    return;

  Tokenizer tok, lineTok;
  if (!tok.setFile(filename.c_str()))
  {
    char str[256];
    sprintf(str, "Couldn't open \"%s\"", filename.c_str());
    ErrorMsg(str);
    return;
  }

  RefArray <glm::vec3> vertices;
  RefArray <glm::vec3> normals;
  RefArray <vec2> texCoords;
  RefArray <glm::vec3> tangents;
  RefArray <glm::vec3> binormals;

  RefArray <unsigned> vtxIndices;
  RefArray <unsigned> nrmIndices;
  RefArray <unsigned> txcIndices;

  RefArray <ObjMaterial> materials;

  ObjMaterial mat;
  mat.name = new char[2];
  strcpy(mat.name, " ");
  mat.indices = new RefArray <unsigned>();
  materials.Add(mat);
  unsigned currIndex = 0;
  unsigned currMaterial = 0;

  unsigned n, vStart, nStart, tStart;
  char *str;

  while ((str = tok.next()) != NULL){
    switch (str[0]){
    case 'f':
      lineTok.setString(tok.nextLine());

      vStart = vtxIndices.GetCount();
      nStart = nrmIndices.GetCount();
      tStart = txcIndices.GetCount();
      n = 0;
      while ((str = lineTok.next()) != NULL){
        if (n > 2){
          vtxIndices.Add(vtxIndices[vStart]);
          vtxIndices.Add(vtxIndices[vtxIndices.GetCount() - 2]);
          if (texCoords.GetCount()){
            txcIndices.Add(txcIndices[tStart]);
            txcIndices.Add(txcIndices[txcIndices.GetCount() - 2]);
          }
          if (normals.GetCount()){
            nrmIndices.Add(nrmIndices[nStart]);
            nrmIndices.Add(nrmIndices[nrmIndices.GetCount() - 2]);
          }
        }

        int index;
        if (str[0] == '-')
          index = vertices.GetCount() - atoi(lineTok.next());
        else
          index = atoi(str) - 1;
        vtxIndices.Add(index);

        if (texCoords.GetCount() || normals.GetCount()) lineTok.goToNext();
        if (texCoords.GetCount()){
          str = lineTok.next();
          // TODO: what does blender's export mean?
          if(!str)
            break;
          if (str[0] == '-')
            index = texCoords.GetCount() - atoi(lineTok.next());
          else
            index = atoi(str) - 1;
          txcIndices.Add(index);
        }
        if (normals.GetCount()){
          lineTok.goToNext();

          str = lineTok.next();
          if (str[0] == '-')
            index = normals.GetCount() - atoi(lineTok.next());
          else
            index = atoi(str) - 1;
          nrmIndices.Add(index);
        }

        n++;
      }
      break;
    case 'v':
      float x, y, z;
      switch (str[1]){
      case '\0':
        x = readFloat(tok);
        y = readFloat(tok);
        z = readFloat(tok);
        vertices.Add(glm::vec3(x, y, z));
        break;
      case 'n':
        x = readFloat(tok);
        y = readFloat(tok);
        z = readFloat(tok);
        normals.Add(glm::vec3(x, y, z));
        break;
      case 't':
        x = readFloat(tok);
        y = readFloat(tok);
        texCoords.Add(vec2(x, y));
        break;
      }
      break;
    case '#':
      // TODO: code below will cause the loader skip the 1st vertex
      //if (*tok.next() == '_'){
      //  tok.goToNext();
      //  str = tok.next();
      //  if (stricmp(str, "tangent") == 0){
      //    x = readFloat(tok);
      //    y = readFloat(tok);
      //    z = readFloat(tok);
      //    tangents.add(glm::vec3(x, y, z));
      //  } else if (stricmp(str, "binormal") == 0){
      //    x = readFloat(tok);
      //    y = readFloat(tok);
      //    z = readFloat(tok);
      //    binormals.add(glm::vec3(x, y, z));
      //  }
      //} else {
      tok.goToNextLine();
      //}
      break;
    case 'u':
      if (vtxIndices.GetCount()){
        materials[currMaterial].indices->Add(currIndex);
        materials[currMaterial].indices->Add(vtxIndices.GetCount() - currIndex);
        currIndex = vtxIndices.GetCount();
      } else {
        // Ensure we don't get an empty material set in the beginning
        delete materials[0].name;
        delete materials[0].indices;
        materials.clear();
      }

      currMaterial = getObjMaterial(materials, tok.next());
      break;
    case 'm':
      if( strcmp(str, "mtllib") == 0 )
      {
        STRING matFilename = tok.nextLine();
        Trim(matFilename);
        doLoadObjMaterial(matFilename, model);
      }
      break;
    default:
      tok.goToNextLine();
      break;
    }
  }

  materials[currMaterial].indices->Add(currIndex);
  materials[currMaterial].indices->Add(vtxIndices.GetCount() - currIndex);

  model.SetIndicesCount(vtxIndices.GetCount());

  model.AddStream(TYPE_VERTEX, 3, vertices.GetCount(), (float *) vertices.AbandonArray(), 
    rearrange(materials, vtxIndices.GetArray(), model.GetIndicesCount()), false);

  // add texture coordinates, tangents and binormals
  if (texCoords.GetCount())
  {
    assert(txcIndices.GetCount() == model.m_nIndices);

    unsigned *indices = rearrange(materials, txcIndices.GetArray(), model.m_nIndices);

    model.AddStream(TYPE_TEXCOORD, 2, texCoords.GetCount(), (float *) texCoords.AbandonArray(), indices, false);
    if (tangents.GetCount()){
      unsigned *tIndices = new unsigned[model.m_nIndices];
      memcpy(tIndices, indices, model.m_nIndices * sizeof(unsigned));

      model.AddStream(TYPE_TEXCOORD, 3, tangents.GetCount(), (float *) tangents.AbandonArray(), tIndices, false);
    }
    if (binormals.GetCount()){
      unsigned *bIndices = new unsigned[model.m_nIndices];
      memcpy(bIndices, indices, model.m_nIndices * sizeof(unsigned));

      model.AddStream(TYPE_TEXCOORD, 3, binormals.GetCount(), (float *) binormals.AbandonArray(), bIndices, false);
    }
  }

  // add normals
  if (normals.GetCount()){
    assert(nrmIndices.GetCount() == model.m_nIndices);

    model.AddStream(TYPE_NORMAL, 3, normals.GetCount(), (float *) normals.AbandonArray(), rearrange(materials, nrmIndices.GetArray(), model.m_nIndices), false);
  }

  // Fix batches
  currIndex = 0;
  unsigned startIndex = 0;
  for (unsigned i = 0; i < materials.GetCount(); i++){
    RefArray <unsigned> *mInds = materials[i].indices;
    for (unsigned j = 0; j < mInds->GetCount(); j += 2){
      currIndex += (*mInds)[j + 1];
    }
    model.AddBatch(startIndex, currIndex - startIndex);
    startIndex = currIndex;

    delete materials[i].indices;
    delete materials[i].name;
  }

  model.MakeDrawable();

  Logger::InstanceRef().Write(LogNormal, "Model loaded: %s.", filename.c_str());
}

void MeshManager::doLoadObjMaterial(STRING filename, Mesh& mesh)
{
  // TODO: load material?
}
